After the tab view hides or shows (including when the chat window first opens, if...
[adiumx.git] / Plugins / Dual Window Interface / AIMessageViewController.m
blobce71150cba4d4684b0e8eb1eae5d9494a375f7f0
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "AIMessageViewController.h"
18 #import "AIAccountSelectionView.h"
19 #import "AIMessageWindowController.h"
20 #import "ESGeneralPreferencesPlugin.h"
21 #import "AIDualWindowInterfacePlugin.h"
22 #import "AIContactInfoWindowController.h"
23 #import "AIMessageTabSplitView.h"
25 #import <Adium/AIChatControllerProtocol.h>
26 #import <Adium/AIContactAlertsControllerProtocol.h>
27 #import <Adium/AIContactControllerProtocol.h>
28 #import <Adium/AIContentControllerProtocol.h>
29 #import <Adium/AIContentControllerProtocol.h>
30 #import <Adium/AIInterfaceControllerProtocol.h>
31 #import <Adium/AIMenuControllerProtocol.h>
32 #import <Adium/AIPreferenceControllerProtocol.h>
33 #import <Adium/AIToolbarControllerProtocol.h>
34 #import <Adium/AIAccount.h>
35 #import <Adium/AIChat.h>
36 #import <Adium/AIContentMessage.h>
37 #import <Adium/AIListContact.h>
38 #import <Adium/AIListObject.h>
39 #import <Adium/AIListOutlineView.h>
40 #import <Adium/AIMessageEntryTextView.h>
41 #import <Adium/ESTextAndButtonsWindowController.h>
43 #import <AIUtilities/AIApplicationAdditions.h>
44 #import <AIUtilities/AIAttributedStringAdditions.h>
45 #import <AIUtilities/AIAutoScrollView.h>
46 #import <AIUtilities/AIDictionaryAdditions.h>
47 #import <AIUtilities/AISplitView.h>
49 #import <AIUtilities/AITigerCompatibility.h>
51 #import <PSMTabBarControl/NSBezierPath_AMShading.h>
52 #import "KNShelfSplitView.h"
53 #import "ESChatUserListController.h"
55 //Heights and Widths
56 #define MESSAGE_VIEW_MIN_HEIGHT_RATIO           .50                                             //Mininum height ratio of the message view
57 #define MESSAGE_VIEW_MIN_WIDTH_RATIO            .50                                             //Mininum width ratio of the message view
58 #define ENTRY_TEXTVIEW_MIN_HEIGHT                       20                                              //Mininum height of the text entry view
59 #define USER_LIST_MIN_WIDTH                                     24                                              //Mininum width of the user list
60 #define USER_LIST_DEFAULT_WIDTH                         120                                             //Default width of the user list
62 //Preferences and files
63 #define MESSAGE_VIEW_NIB                                        @"MessageView"                  //Filename of the message view nib
64 #define USERLIST_THEME                                          @"UserList Theme"               //File name of the user list theme
65 #define USERLIST_LAYOUT                                         @"UserList Layout"              //File name of the user list layout
66 #define KEY_ENTRY_TEXTVIEW_MIN_HEIGHT           @"Minimum Text Height"  //Preference key for text entry height
67 #define KEY_ENTRY_USER_LIST_MIN_WIDTH           @"UserList Width"               //Preference key for user list width
70 @interface AIMessageViewController (PRIVATE)
71 - (id)initForChat:(AIChat *)inChat;
72 - (void)chatStatusChanged:(NSNotification *)notification;
73 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification;
74 - (void)_configureMessageDisplay;
75 - (void)_createAccountSelectionView;
76 - (void)_destroyAccountSelectionView;
77 - (void)_configureTextEntryView;
78 - (void)_updateTextEntryViewHeight;
79 - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum;
80 - (void)_showUserListView;
81 - (void)_hideUserListView;
82 - (void)_configureUserList;
83 - (void)_updateUserListViewWidth;
84 - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum;
85 - (void)updateFramesForAccountSelectionView;
86 - (void)saveUserListMinimumSize;
87 @end
89 @implementation AIMessageViewController
91 /*!
92  * @brief Create a new message view controller
93  */
94 + (AIMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat
96     return [[[self alloc] initForChat:inChat] autorelease];
101  * @brief Initialize
102  */
103 - (id)initForChat:(AIChat *)inChat
105     if ((self = [super init])) {
106                 AIListContact   *contact;
107                 //Init
108                 chat = [inChat retain];
109                 contact = [chat listObject];
110                 view_accountSelection = nil;
111                 userListController = nil;
112                 suppressSendLaterPrompt = NO;
113                 retainingScrollViewUserList = NO;
114                 
115                 //Load the view containing our controls
116                 [NSBundle loadNibNamed:MESSAGE_VIEW_NIB owner:self];
117                 
118                 //Register for the various notification we need
119                 [[adium notificationCenter] addObserver:self
120                                                                            selector:@selector(sendMessage:) 
121                                                                                    name:Interface_SendEnteredMessage
122                                                                                  object:chat];
123                 [[adium notificationCenter] addObserver:self
124                                                                            selector:@selector(didSendMessage:)
125                                                                                    name:Interface_DidSendEnteredMessage 
126                                                                                  object:chat];
127                 [[adium notificationCenter] addObserver:self
128                                                                            selector:@selector(chatStatusChanged:) 
129                                                                                    name:Chat_StatusChanged
130                                                                                  object:chat];
131                 [[adium notificationCenter] addObserver:self 
132                                                                            selector:@selector(chatParticipatingListObjectsChanged:)
133                                                                                    name:Chat_ParticipatingListObjectsChanged
134                                                                                  object:chat];
135                 [[adium notificationCenter] addObserver:self
136                                                                            selector:@selector(redisplaySourceAndDestinationSelector:) 
137                                                                                    name:Chat_SourceChanged
138                                                                                  object:chat];
139                 [[adium notificationCenter] addObserver:self
140                                                                            selector:@selector(redisplaySourceAndDestinationSelector:) 
141                                                                                    name:Chat_DestinationChanged
142                                                                                  object:chat];
143                 [[adium notificationCenter] addObserver:self
144                                                                            selector:@selector(toggleUserlist:)
145                                                                                    name:@"toggleUserlist"
146                                                                                  object:nil];
147                 
148                 [splitView_textEntryHorizontal setDividerThickness:3]; //Default is 9
149                 [splitView_textEntryHorizontal setDrawsDivider:NO];
150                 
151                 //Observe general preferences for sending keys
152                 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_GENERAL];
154                 /* Update chat status and participating list objects to configure the user list if necessary
155                  * Call chatParticipatingListObjectsChanged first, which will set up the user list. This allows other sizing to match.
156                  */
157                 [self setUserListVisible:[chat isGroupChat]];
158                 
159                 [self chatParticipatingListObjectsChanged:nil];
160                 [self chatStatusChanged:nil];
161                 
162                 //Configure our views
163                 [self _configureMessageDisplay];
164                 [self _configureTextEntryView];
166                 //Set our base writing direction
167                 if (contact) {
168                         [textView_outgoing setBaseWritingDirection:[contact baseWritingDirection]];
169                 }
170         }
172         return self;
176  * @brief Deallocate
177  */
178 - (void)dealloc
179 {   
180         AIListContact   *contact = [chat listObject];
181         
182         [[adium preferenceController] unregisterPreferenceObserver:self];
184         //Store our minimum height for the text entry area, and minimim width for the user list
185         [[adium preferenceController] setPreference:[NSNumber numberWithInt:entryMinHeight]
186                                                                                  forKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
187                                                                                   group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
189         if (userListController) {
190                 [self saveUserListMinimumSize];
191         }
192         
193         //Save the base writing direction
194         if (contact)
195                 [contact setBaseWritingDirection:[textView_outgoing baseWritingDirection]];
197         [chat release]; chat = nil;
199     //remove observers
200     [[adium notificationCenter] removeObserver:self];
201     [[NSNotificationCenter defaultCenter] removeObserver:self];
202         
203     //Account selection view
204         [self _destroyAccountSelectionView];
205         
206         [messageDisplayController messageViewIsClosing];
207     [messageDisplayController release];
208         [userListController release];
210         [controllerView_messages release];
211         
212         //Release view_contents, for which we are responsible because we loaded it via -[NSBundle loadNibNamed:owner]
213         [view_contents release];
215         //Release the hidden user list view
216         if (retainingScrollViewUserList) {
217                 [scrollView_userList release];
218         }
219         //release menuItem
220         [showHide release];
221     [super dealloc];
224 - (void)saveUserListMinimumSize
226         [[adium preferenceController] setPreference:[NSNumber numberWithInt:userListMinWidth]
227                                                                                  forKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
228                                                                                   group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
231 - (void)updateGradientColors
233         NSColor *darkerColor = [NSColor colorWithCalibratedWhite:0.90 alpha:1.0];
234         NSColor *lighterColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1.0];
235         NSColor *leftColor = nil, *rightColor = nil;
237         switch ([messageWindowController tabPosition]) {
238                 case AdiumTabPositionBottom:
239                 case AdiumTabPositionTop:
240                 case AdiumTabPositionLeft:
241                         leftColor = lighterColor;
242                         rightColor = darkerColor;
243                         break;
244                 case AdiumTabPositionRight:
245                         leftColor = darkerColor;
246                         rightColor = lighterColor;
247                         break;
248         }
250         [view_accountSelection setLeftColor:leftColor rightColor:rightColor];
251         [splitView_textEntryHorizontal setLeftColor:leftColor rightColor:rightColor];
255  * @brief Invoked before the message view closes
257  * This method is invoked before our message view controller's message view leaves a window.
258  * We need to clean up our user list to invalidate cursor tracking before the view closes.
259  */
260 - (void)messageViewWillLeaveWindowController:(AIMessageWindowController *)inWindowController
262         if (inWindowController) {
263                 [userListController contactListWillBeRemovedFromWindow];
264         }
265         
266         [messageWindowController release]; messageWindowController = nil;
269 - (void)messageViewAddedToWindowController:(AIMessageWindowController *)inWindowController
271         if (inWindowController) {
272                 [userListController contactListWasAddedBackToWindow];
273         }
274         
275         if (inWindowController != messageWindowController) {
276                 [messageWindowController release];
277                 messageWindowController = [inWindowController retain];
278                 
279                 [self updateGradientColors];
280         }
284  * @brief Retrieve the chat represented by this message view
285  */
286 - (AIChat *)chat
288     return chat;
292  * @brief Retrieve the source account associated with this chat
293  */
294 - (AIAccount *)account
296     return [chat account];
300  * @brief Retrieve the destination list object associated with this chat
301  */
302 - (AIListContact *)listObject
304     return [chat listObject];
308  * @brief Returns the selected list object in our participants list
309  */
310 - (AIListObject *)preferredListObject
312         if (userListView) { //[[shelfView subviews] containsObject:scrollView_userList] && ([userListView selectedRow] != -1)
313                 return [userListView itemAtRow:[userListView selectedRow]];
314         }
315         
316         return nil;
320  * @brief Invoked when the status of our chat changes
322  * The only chat status change we're interested in is one to the disallow account switching flag.  When this flag 
323  * changes we update the visibility of our account status menus accordingly.
324  */
325 - (void)chatStatusChanged:(NSNotification *)notification
327     NSArray     *modifiedKeys = [[notification userInfo] objectForKey:@"Keys"];
328         
329     if (notification == nil || [modifiedKeys containsObject:@"DisallowAccountSwitching"]) {
330                 [self setAccountSelectionMenuVisibleIfNeeded:YES];
331     }
335 //Message Display ------------------------------------------------------------------------------------------------------
336 #pragma mark Message Display
338  * @brief Configure the message display view
339  */
340 - (void)_configureMessageDisplay
342         //Create the message view
343         messageDisplayController = [[[adium interfaceController] messageDisplayControllerForChat:chat] retain];
344         //Get the messageView from the controller
345         controllerView_messages = [[messageDisplayController messageView] retain];
346         //scrollView_messages is originally a placeholder; replace it with controllerView_messages
347         [controllerView_messages setFrame:[scrollView_messages documentVisibleRect]];
348         [[customView_messages superview] replaceSubview:customView_messages with:controllerView_messages];
350         //This is what draws our transparent background
351         //Technically, it could be set in MessageView.nib, too
352         [scrollView_messages setBackgroundColor:[NSColor clearColor]];
354         [controllerView_messages setNextResponder:textView_outgoing];
358  * @brief Access to our view
359  */
360 - (NSView *)view
362     return view_contents;
366  * @brief Support for printing.  Forward the print command to our message display view
367  */
368 - (void)adiumPrint:(id)sender
370         if ([messageDisplayController respondsToSelector:@selector(adiumPrint:)]) {
371                 [messageDisplayController adiumPrint:sender];
372         }
376 //Messaging ------------------------------------------------------------------------------------------------------------
377 #pragma mark Messaging
379  * @brief Send the entered message
380  */
381 - (IBAction)sendMessage:(id)sender
383         NSAttributedString      *attributedString = [textView_outgoing textStorage];
384         
385         //Only send if we have a non-zero-length string
386     if ([attributedString length] != 0) { 
387                 AIListObject                            *listObject = [chat listObject];
388                 
389                 if ([chat isGroupChat] && ![[chat account] online]) {
390                         //Refuse to do anything with a group chat for an offline account.
391                         NSBeep();
392                         return;
393                 }
394                 
395                 if (!suppressSendLaterPrompt &&
396                         ![chat canSendMessages]) {
397                         
398                         NSString                                                        *formattedUID = [listObject formattedUID];
400                         NSAlert *alert = [[NSAlert alloc] init];
401                         [alert setMessageText:[NSString stringWithFormat:AILocalizedString(@"%@ appears to be offline. How do you want to send this message?", nil),
402                                                                    formattedUID]];
403                         [alert setInformativeText:[NSString stringWithFormat:
404                                                                            AILocalizedString(@"Send Later will send the message the next time both you and %@ are online. Send Now may work if %@ is invisible or is not on your contact list and so only appears to be offline.", "Send Later dialogue explanation text"),
405                                                                            formattedUID, formattedUID, formattedUID]];
406                         [alert addButtonWithTitle:AILocalizedString(@"Send Now", nil)];
408                         [alert addButtonWithTitle:AILocalizedString(@"Send Later", nil)];
409                         [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"l"];
410                         [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0];
412                         [alert addButtonWithTitle:AILocalizedString(@"Don't Send", nil)];                        
413                         [[[alert buttons] objectAtIndex:2] setKeyEquivalent:@"\E"];
414                         [[[alert buttons] objectAtIndex:2] setKeyEquivalentModifierMask:0];
416                         NSImage *icon = ([listObject userIcon] ? [listObject userIcon] : [AIServiceIcons serviceIconForObject:listObject
417                                                                                                                                                                                                                          type:AIServiceIconLarge
418                                                                                                                                                                                                                 direction:AIIconNormal]);
419                         icon = [[icon copy] autorelease];
420                         [icon setScalesWhenResized:NO];
421                         [alert setIcon:icon];
422                         [alert setAlertStyle:NSInformationalAlertStyle];
424                         [alert beginSheetModalForWindow:[view_contents window]
425                                                           modalDelegate:self
426                                                          didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
427                                                                 contextInfo:NULL];
428                         [alert release];
431                 } else {
432                         AIContentMessage                *message;
433                         NSAttributedString              *outgoingAttributedString;
434                         AIAccount                               *account = [chat account];
435                         //Send the message
436                         [[adium notificationCenter] postNotificationName:Interface_WillSendEnteredMessage
437                                                                                                           object:chat
438                                                                                                         userInfo:nil];
440                         outgoingAttributedString = [attributedString copy];
441                         message = [AIContentMessage messageInChat:chat
442                                                                                    withSource:account
443                                                                                   destination:[chat listObject]
444                                                                                                  date:nil //created for us by AIContentMessage
445                                                                                           message:outgoingAttributedString
446                                                                                         autoreply:NO];
447                         [outgoingAttributedString release];
449                         if ([[adium contentController] sendContentObject:message]) {
450                                 [[adium notificationCenter] postNotificationName:Interface_DidSendEnteredMessage 
451                                                                                                                   object:chat
452                                                                                                                 userInfo:nil];
453                         }
454                 }
455     }
459  * @brief Send Later button was pressed
460  */ 
461 - (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
463         switch (returnCode) {
464                 case NSAlertFirstButtonReturn: /* Send Now */
465                         suppressSendLaterPrompt = YES;
466                         [self sendMessage:nil];
467                         break;
468                         
469                 case NSAlertSecondButtonReturn: /* Send Later */
470                         [self sendMessageLater:nil];
471                         break;
472                 case NSAlertThirdButtonReturn: /* Don't Send */
473                         break;          
474         }
478  * @brief Invoked after our entered message sends
480  * This method hides the account selection view and clears the entered message after our message sends
481  */
482 - (IBAction)didSendMessage:(id)sender
484     [self setAccountSelectionMenuVisibleIfNeeded:NO];
485     [self clearTextEntryView];
486         
487         //Redisplay the cursor
488         [NSCursor setHiddenUntilMouseMoves:NO];
492  * @brief Offline messaging
493  */
494 - (IBAction)sendMessageLater:(id)sender
496         AIListContact   *listContact;
498         //If the chat can _now_ send a message, send it immediately instead of waiting for "later".
499         if ([chat canSendMessages]) {
500                 [self sendMessage:sender];
501                 return;
502         }
504         //Put the alert on the metaContact containing this listContact if applicable
505         listContact = [[chat listObject] parentContact];
507         if (listContact) {
508                 NSMutableDictionary *detailsDict, *alertDict;
509                 
510                 detailsDict = [NSMutableDictionary dictionary];
511                 [detailsDict setObject:[[chat account] internalObjectID] forKey:@"Account ID"];
512                 [detailsDict setObject:[NSNumber numberWithBool:YES] forKey:@"Allow Other"];
513                 [detailsDict setObject:[listContact internalObjectID] forKey:@"Destination ID"];
515                 alertDict = [NSMutableDictionary dictionary];
516                 [alertDict setObject:detailsDict forKey:@"ActionDetails"];
517                 [alertDict setObject:CONTACT_SEEN_ONLINE_YES forKey:@"EventID"];
518                 [alertDict setObject:@"SendMessage" forKey:@"ActionID"];
519                 [alertDict setObject:[NSNumber numberWithBool:YES] forKey:@"OneTime"]; 
520                 
521                 [alertDict setObject:listContact forKey:@"TEMP-ListContact"];
522                 
523                 [[adium contentController] filterAttributedString:[[[textView_outgoing textStorage] copy] autorelease]
524                                                                                   usingFilterType:AIFilterContent
525                                                                                                 direction:AIFilterOutgoing
526                                                                                         filterContext:listContact
527                                                                                   notifyingTarget:self
528                                                                                                  selector:@selector(gotFilteredMessageToSendLater:receivingContext:)
529                                                                                                   context:alertDict];
531                 [self didSendMessage:nil];
532         }
536  * @brief Offline messaging
537  */
538 //XXX - Offline messaging code SHOULD NOT BE IN HERE! -ai
539 - (void)gotFilteredMessageToSendLater:(NSAttributedString *)filteredMessage receivingContext:(NSMutableDictionary *)alertDict
541         NSMutableDictionary     *detailsDict;
542         AIListContact           *listContact;
543         
544         detailsDict = [alertDict objectForKey:@"ActionDetails"];
545         [detailsDict setObject:[filteredMessage dataRepresentation] forKey:@"Message"];
547         listContact = [[alertDict objectForKey:@"TEMP-ListContact"] retain];
548         [alertDict removeObjectForKey:@"TEMP-ListContact"];
549         
550         [[adium contactAlertsController] addAlert:alertDict 
551                                                                  toListObject:listContact
552                                                          setAsNewDefaults:NO];
553         [listContact release];
556 //Account Selection ----------------------------------------------------------------------------------------------------
557 #pragma mark Account Selection
559  * @brief
560  */
561 - (void)accountSelectionViewFrameDidChange:(NSNotification *)notification
563         [self updateFramesForAccountSelectionView];
567  * @brief Redisplay the source/destination account selector
568  */
569 - (void)redisplaySourceAndDestinationSelector:(NSNotification *)notification
571         [self setAccountSelectionMenuVisibleIfNeeded:YES];
575  * @brief Toggle visibility of the account selection menus
577  * Invoking this method with NO will hide the account selection menus.  Invoking it with YES will show the account
578  * selection menus if they are needed.
579  */
580 - (void)setAccountSelectionMenuVisibleIfNeeded:(BOOL)makeVisible
582         //Hide or show the account selection view as requested
583         if (makeVisible) {
584                 [self _createAccountSelectionView];
585         } else {
586                 [self _destroyAccountSelectionView];
587         }
591  * @brief Show the account selection view
592  */
593 - (void)_createAccountSelectionView
595         if (!view_accountSelection) {
596                 NSRect  contentFrame = [splitView_textEntryHorizontal frame];
598                 //Create the account selection view and insert it into our window
599                 view_accountSelection = [[AIAccountSelectionView alloc] initWithFrame:contentFrame];
601                 [view_accountSelection setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
602                 
603                 [self updateGradientColors];
604                 
605                 //Insert the account selection view at the top of our view
606                 [[shelfView contentView] addSubview:view_accountSelection];
607                 [view_accountSelection setChat:chat];
609                 [[NSNotificationCenter defaultCenter] addObserver:self
610                                                                                                  selector:@selector(accountSelectionViewFrameDidChange:)
611                                                                                                          name:AIViewFrameDidChangeNotification
612                                                                                                    object:view_accountSelection];
613                 
614                 [self updateFramesForAccountSelectionView];
615                         
616                 //Redisplay everything
617                 [[shelfView contentView] setNeedsDisplay:YES];
618         }
622  * @brief Hide the account selection view
623  */
624 - (void)_destroyAccountSelectionView
626         if (view_accountSelection) {
627                 //Remove the observer
628                 [[NSNotificationCenter defaultCenter] removeObserver:self
629                                                                                                                 name:AIViewFrameDidChangeNotification
630                                                                                                           object:view_accountSelection];
632                 //Remove the account selection view from our window, clean it up
633                 [view_accountSelection removeFromSuperview];
634                 [view_accountSelection release]; view_accountSelection = nil;
636                 //Redisplay everything
637                 [self updateFramesForAccountSelectionView];
638         }
642  * @brief Position the account selection view, if it is present, and the messages/text entry splitview appropriately
643  */
644 - (void)updateFramesForAccountSelectionView
646         int             contentsHeight = [[shelfView contentView] frame].size.height;
647         int     accountSelectionHeight = (view_accountSelection ? [view_accountSelection frame].size.height : 0);
648         int             intersectionPoint = ([[shelfView contentView] isFlipped] ? accountSelectionHeight : (contentsHeight - accountSelectionHeight));
650         if (view_accountSelection) {
651                 [view_accountSelection setFrameOrigin:NSMakePoint(NSMinX([view_accountSelection frame]), intersectionPoint)];
652                 [view_accountSelection setNeedsDisplay:YES];
653         }
655         [splitView_textEntryHorizontal setFrameSize:NSMakeSize(NSWidth([splitView_textEntryHorizontal frame]), intersectionPoint)];
656         [splitView_textEntryHorizontal setNeedsDisplay:YES];
657 }       
660 //Text Entry -----------------------------------------------------------------------------------------------------------
661 #pragma mark Text Entry
663  * @brief Preferences changed, update sending keys
664  */
665 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
666                                         preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
668         [textView_outgoing setSendOnReturn:[[prefDict objectForKey:SEND_ON_RETURN] boolValue]];
669         [textView_outgoing setSendOnEnter:[[prefDict objectForKey:SEND_ON_ENTER] boolValue]];
673  * @brief Configure the text entry view
674  */
675 - (void)_configureTextEntryView
676 {       
677         //Configure the text entry view
678     [textView_outgoing setTarget:self action:@selector(sendMessage:)];
680         //This is necessary for tab completion.
681         [textView_outgoing setDelegate:self];
682     
683         [textView_outgoing setTextContainerInset:NSMakeSize(0,2)];
684     if ([textView_outgoing respondsToSelector:@selector(setUsesFindPanel:)]) {
685                 [textView_outgoing setUsesFindPanel:YES];
686     }
687         [textView_outgoing setClearOnEscape:YES];
688         [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]];
689         
690         //User's choice of mininum height for their text entry view
691         entryMinHeight = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
692                                                                                                                            group:PREF_GROUP_DUAL_WINDOW_INTERFACE] intValue];
693         if (entryMinHeight <= 0) entryMinHeight = [self _textEntryViewProperHeightIgnoringUserMininum:YES];
695         //Associate the view with our message view so it knows which view to scroll in response to page up/down
696         //and other special key-presses.
697         [textView_outgoing setAssociatedView:[messageDisplayController messageScrollView]];
698         
699         //Associate the text entry view with our chat and inform Adium that it exists.
700         //This is necessary for text entry filters to work correctly.
701         [textView_outgoing setChat:chat];
702         
703     //Observe text entry view size changes so we can dynamically resize as the user enters text
704     [[NSNotificationCenter defaultCenter] addObserver:self
705                                                                                          selector:@selector(outgoingTextViewDesiredSizeDidChange:)
706                                                                                                  name:AIViewDesiredSizeDidChangeNotification 
707                                                                                            object:textView_outgoing];
709         [self _updateTextEntryViewHeight];
713  * @brief Sets our text entry view as the first responder
714  */
715 - (void)makeTextEntryViewFirstResponder
717     [[textView_outgoing window] makeFirstResponder:textView_outgoing];
721  * @brief Clear the message entry text view
722  */
723 - (void)clearTextEntryView
725         NSWritingDirection      writingDirection;
727         writingDirection = [textView_outgoing baseWritingDirection];
728         
729         [textView_outgoing setString:@""];
730         [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]];
731         
732         [textView_outgoing setBaseWritingDirection:writingDirection];   //Preserve the writing diraction
734     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
735                                                                                                                 object:textView_outgoing];
739  * @brief Add text to the message entry text view 
741  * Adds the passed string to the entry text view at the insertion point.  If there is selected text in the view, it
742  * will be replaced.
743  */
744 - (void)addToTextEntryView:(NSAttributedString *)inString
746     [textView_outgoing insertText:inString];
747     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
751  * @brief Add data to the message entry text view 
753  * Adds the passed pasteboard data to the entry text view at the insertion point.  If there is selected text in the
754  * view, it will be replaced.
755  */
756 - (void)addDraggedDataToTextEntryView:(id <NSDraggingInfo>)draggingInfo
758     [textView_outgoing performDragOperation:draggingInfo];
759     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
763  * @brief Update the text entry view's height when its desired size changes
764  */
765 - (void)outgoingTextViewDesiredSizeDidChange:(NSNotification *)notification
767         [self _updateTextEntryViewHeight];
770 - (void)tabViewDidChangeVisibility
772         [self _updateTextEntryViewHeight];
775 /* 
776  * @brief Update the height of our text entry view
778  * This method sets the height of the text entry view to the most ideal value, and adjusts the other views in our
779  * window to fill the remaining space.
780  */
781 - (void)_updateTextEntryViewHeight
783         int             height = [self _textEntryViewProperHeightIgnoringUserMininum:NO];
784         
785         //Display the vertical scroller if our view is not tall enough to display all the entered text
786         [scrollView_outgoing setHasVerticalScroller:(height < [textView_outgoing desiredSize].height)];
788         if ([NSApp isOnLeopardOrBetter]) {
789                 //Attempt to maximize the message view's size.  We'll automatically restrict it to the correct minimum via the NSSplitView's delegate methods.
790                 [splitView_textEntryHorizontal setPosition:NSHeight([splitView_textEntryHorizontal frame])
791                                                                   ofDividerAtIndex:0];
792                 
793         } else {
794                 NSRect  tempFrame, newFrame;
795                 BOOL    changed = NO;
797                 //Size the outgoing text view to the desired height
798                 tempFrame = [scrollView_outgoing frame];
799                 newFrame = NSMakeRect(tempFrame.origin.x,
800                                                           [splitView_textEntryHorizontal frame].size.height - height,
801                                                           tempFrame.size.width,
802                                                           height);
803                 if (!NSEqualRects(tempFrame, newFrame)) {
804                         [scrollView_outgoing setFrame:newFrame];
805                         [scrollView_outgoing setNeedsDisplay:YES];
806                         changed = YES;
807                 }
809                 if (changed) {
810                         [splitView_textEntryHorizontal adjustSubviews];
811                 }
812         }
816  * @brief Returns the height our text entry view should be
818  * This method takes into account user preference, the amount of entered text, and the current window size to return
819  * a height which is most ideal for the text entry view.
821  * @param ignoreUserMininum If YES, the user's preference for mininum height will be ignored
822  */
823 - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum
825         int dividerThickness = [splitView_textEntryHorizontal dividerThickness];
826         int allowedHeight = ([splitView_textEntryHorizontal frame].size.height / 2.0) - dividerThickness;
827         int     height;
828         
829         //Our primary goal is to display all the entered text
830         height = [textView_outgoing desiredSize].height;
831         
832         //But we must never fall below the user's prefered mininum or above the allowed height
833         if (!ignoreUserMininum && height < entryMinHeight) height = entryMinHeight;
834         if (height > allowedHeight) height = allowedHeight;
835         
836         return height;
839 #pragma mark Autocompletion
841  * @brief Should the tab key cause an autocompletion if possible?
843  * We only tab to autocomplete for a group chat
844  */
845 - (BOOL)textViewShouldTabComplete:(NSTextView *)inTextView
847         return [[self chat] isGroupChat];
850 - (NSArray *)textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index
852         NSMutableArray  *completions;
853         
854         if ([[self chat] isGroupChat]) {
855                 NSString                *partialWord = [[[textView textStorage] attributedSubstringFromRange:charRange] string];
856                 NSEnumerator    *enumerator;
857                 AIListContact   *listContact;
858                 
859                 NSString                *suffix;
860                 if (charRange.location == 0) {
861                         //At the start of a line, append ": "
862                         suffix = @": ";
863                 } else {
864                         suffix = nil;
865                 }
866                 
867                 completions = [NSMutableArray array];
868                 enumerator = [[[self chat] containedObjects] objectEnumerator];
869                 while ((listContact = [enumerator nextObject])) {
870                         if ([[listContact displayName] rangeOfString:partialWord
871                                                                                                  options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
872                                 
873                                 [completions addObject:(suffix ? [[listContact displayName] stringByAppendingString:suffix] : [listContact displayName])];
874                                 
875                         } else if ([[listContact formattedUID] rangeOfString:partialWord
876                                                                                                                  options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
877                                 [completions addObject:(suffix ? [[listContact formattedUID] stringByAppendingString:suffix] : [listContact formattedUID])];
878                                 
879                         } else if ([[listContact UID] rangeOfString:partialWord
880                                                                                                 options:(NSLiteralSearch | NSAnchoredSearch)].location != NSNotFound) {
881                                 [completions addObject:(suffix ? [[listContact UID] stringByAppendingString:suffix] : [listContact UID])];
882                         }
883                 }
885                 if ([completions count]) {                      
886                         *index = 0;
887                 }
889         } else {
890                 completions = nil;
891         }
893         return ([completions count] ? completions : words);
896 //User List ------------------------------------------------------------------------------------------------------------
897 #pragma mark User List
899  * @brief Set visibility of the user list
900  */
901 - (void)setUserListVisible:(BOOL)inVisible
903         if (inVisible) {
904                 [self _showUserListView];
905         } else {
906                 [self _hideUserListView];
907         }
911  * @brief Returns YES if the user list is currently visible
912  */
913 - (BOOL)userListVisible
915         return [shelfView isShelfVisible];
919  * @brief Show the user list
920  */
921 - (void)_showUserListView
922 {       
923         [self setupShelfView];
925         //Configure the user list
926         [self _configureUserList];
928         //Add the user list back to our window if it's missing
929         if (![self userListVisible]) {
930                 [self _updateUserListViewWidth];
931                 
932                 if (retainingScrollViewUserList) {
933                         [scrollView_userList release];
934                         retainingScrollViewUserList = NO;
935                 }
936         }
940  * @brief Hide the user list.
942  * We gain responsibility for releasing scrollView_userList after we hide it
943  */
944 - (void)_hideUserListView
946         if ([self userListVisible]) {
947                 [scrollView_userList retain];
948                 [scrollView_userList removeFromSuperview];
949                 retainingScrollViewUserList = YES;
950                 
951                 [self saveUserListMinimumSize];
952                 [userListController release];
953                 userListController = nil;
954         
955                 //need to collapse the splitview
956                 [shelfView setShelfIsVisible:NO];
957         }
961  * @brief Configure the user list
963  * Configures the user list view and prepares it for display.  If the user list is not being shown, this configuration
964  * should be avoided for performance.
965  */
966 - (void)_configureUserList
968         if (!userListController) {
969                 NSDictionary    *themeDict = [NSDictionary dictionaryNamed:USERLIST_THEME forClass:[self class]];
970                 NSDictionary    *layoutDict = [NSDictionary dictionaryNamed:USERLIST_LAYOUT forClass:[self class]];
971                 
972                 //Create and configure a controller to manage the user list
973                 userListController = [[ESChatUserListController alloc] initWithContactListView:userListView
974                                                                                                                                                   inScrollView:scrollView_userList 
975                                                                                                                                                           delegate:self];
976                 [userListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
977                 [userListController setContactListRoot:chat];
978                 [userListController setHideRoot:YES];
980                 //User's choice of mininum width for their user list view
981                 userListMinWidth = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
982                                                                                                                                          group:PREF_GROUP_DUAL_WINDOW_INTERFACE] intValue];
983                 if (userListMinWidth < USER_LIST_MIN_WIDTH) userListMinWidth = USER_LIST_DEFAULT_WIDTH;
984                 [shelfView setShelfWidth:[userListView bounds].size.width];
985         }
989  * @brief Update the user list in response to changes
991  * This method is invoked when the chat's participating contacts change.  In resopnse, it sets correct visibility of
992  * the user list, and updates the displayed users.
993  */
994 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification
996     //Update the user list
997         AILogWithSignature(@"%i, so %@ %@",[self userListVisible], ([self userListVisible] ? @"reloading" : @"not reloading"),
998                                            userListController);
999     if ([self userListVisible]) {
1000         [userListController reloadData];
1001     }
1005  * @brief The selection in the user list changed
1007  * When the user list selection changes, we update the chat's "preferred list object", which is used
1008  * elsewhere to identify the currently 'selected' contact for Get Info, Messaging, etc.
1009  */
1010 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1012         if ([notification object] == userListView) {
1013                 int selectedIndex = [userListView selectedRow];
1014                 [chat setPreferredListObject:((selectedIndex != -1) ? 
1015                                                                           [[chat containedObjects] objectAtIndex:selectedIndex] :
1016                                                                           nil)];
1017         }
1021  * @brief Perform default action on the selected user list object
1023  * Here we could open a private message or display info for the user, however we perform no action
1024  * at the moment.
1025  */
1026 - (void)performDefaultActionOnSelectedObject:(AIListObject *)listObject sender:(NSOutlineView *)sender
1028         //Empty
1031 /* 
1032  * @brief Update the width of our user list view
1034  * This method sets the width of the user list view to the most ideal value, and adjusts the other views in our
1035  * window to fill the remaining space.
1036  */
1037 - (void)_updateUserListViewWidth
1039         int             width = [self _userListViewProperWidthIgnoringUserMininum:NO];
1040         int             widthWithDivider = 1 + width;   //resize bar effective width  
1041         NSRect  tempFrame;
1043         //Size the user list view to the desired width
1044         tempFrame = [scrollView_userList frame];
1045         [scrollView_userList setFrame:NSMakeRect([shelfView frame].size.width - width,
1046                                                                                          tempFrame.origin.y,
1047                                                                                          width,
1048                                                                                          tempFrame.size.height)];
1049         
1050         //Size the message view to fill the remaining space
1051         tempFrame = [scrollView_messages frame];
1052         [scrollView_messages setFrame:NSMakeRect(tempFrame.origin.x,
1053                                                                                          tempFrame.origin.y,
1054                                                                                          [shelfView frame].size.width - widthWithDivider,
1055                                                                                          tempFrame.size.height)];
1057         //Redisplay both views and the divider
1058         [shelfView setNeedsDisplay:YES];
1062  * @brief Returns the width our user list view should be
1064  * This method takes into account user preference and the current window size to return a width which is most
1065  * ideal for the user list view.
1067  * @param ignoreUserMininum If YES, the user's preference for mininum width will be ignored
1068  */
1069 - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum
1071         int dividerThickness = 1; //[shelfView dividerThickness];
1072         int allowedWidth = ([shelfView frame].size.width / 2.0) - dividerThickness;
1073         int     width = USER_LIST_MIN_WIDTH;
1074         
1075         //We must never fall below the user's prefered mininum or above the allowed width
1076         if (!ignoreUserMininum && width < userListMinWidth) width = userListMinWidth;
1077         if (width > allowedWidth) width = allowedWidth;
1079         return width;
1083 //Split Views --------------------------------------------------------------------------------------------------
1084 #pragma mark Split Views
1085 /* 
1086  * @brief Returns the maximum constraint of the split pane
1088  * For the horizontal split, we prevent the message view from growing so large that the text entry view
1089  * is forced below its desired height.
1090  */
1091 - (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset
1093         if (sender == splitView_textEntryHorizontal) {
1094                 return ([sender frame].size.height - ([self _textEntryViewProperHeightIgnoringUserMininum:YES] +
1095                                                                                          [sender dividerThickness]));
1097         } else {
1098                 NSLog(@"Unknown split view %@",sender);
1099                 return 0;
1100         }
1103 /* 
1104  * @brief Returns the mininum constraint of the split pane
1106  * For both splitpanes, we prevent the message view from dropping below 50% of the window's width and height
1107  */
1108 - (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset
1110         if (sender == splitView_textEntryHorizontal) {
1111                 return (int)([sender frame].size.height * MESSAGE_VIEW_MIN_HEIGHT_RATIO);
1112                 
1113         } else {
1114                 NSLog(@"Unknown split view %@",sender);
1115                 return 0;
1116         }
1120  * @brief A split view had its divider position changed
1122  * Remember the user's choice of text entry view height.
1123  */
1124 - (float)splitView:(NSSplitView *)sender constrainSplitPosition:(float)proposedPosition ofSubviewAt:(int)index
1126         if (sender == splitView_textEntryHorizontal) {
1127                 entryMinHeight = (int)([sender frame].size.height - (proposedPosition + [sender dividerThickness]));
1128         } else {
1129                 NSLog(@"Unknown split view %@",sender);
1130                 return 0;
1131         }
1132         
1133         return proposedPosition;
1136 /* 
1137  * @brief Returns YES if the passed subview can be collapsed
1138  */
1139 - (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview
1141         if (sender == splitView_textEntryHorizontal) {
1142                 return NO;
1143                 
1144         } else {
1145                 NSLog(@"Unknown split view %@",sender);
1146                 return 0;
1147         }
1150 #pragma mark Shelfview
1151 /* @name        setupShelfView
1152  * @brief       sets up shelfsplitview containing userlist & contentviews
1153  */
1154  -(void)setupShelfView
1156         [shelfView setShelfWidth:200];
1158         AILogWithSignature(@"ShelfView %@ (content view is %@) --> superview %@, in window %@; frame %@; content view %@ shelf view %@ in window %@",
1159                                            shelfView, [shelfView contentView], [shelfView superview], [shelfView window], NSStringFromRect([[shelfView superview] frame]),
1160                                            splitView_textEntryHorizontal,
1161                                            scrollView_userList, [scrollView_userList window]);
1163         [shelfView bind:@"contextButtonMenu" toObject:[self chat] withKeyPath:@"actionMenu"
1164                         options:[NSDictionary dictionaryWithObjectsAndKeys:
1165                                          [NSNumber numberWithBool:YES], NSAllowsNullArgumentBindingOption,
1166                                          [NSNumber numberWithBool:YES], NSValidatesImmediatelyBindingOption,
1167                                          nil]];
1168         [shelfView setContextButtonImage:[NSImage imageNamed:@"sidebarActionWidget.png"]];
1170         [shelfView setShelfIsVisible:YES];
1173 /* @name        toggleUserlist
1174  * @brief       toggles the state of the userlist shelf
1175  */
1176 -(void)toggleUserlist:(id)sender
1177 {       
1178         [shelfView setShelfIsVisible:![shelfView isShelfVisible]];
1179 }       
1181 @end